import json

from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException, \
    InvalidElementStateException, NoSuchAttributeException, ElementNotVisibleException, ElementNotInteractableException, \
    ElementClickInterceptedException

from .errortype import ErrorType
from robot.api import logger

from .errorcode import ErrorCode
from ..localization.message import Msg


class TestException(Exception):

    def __init__(self, msg: str = '', err_code: str = '', err_class: str = ErrorType.ZCM_LIB, stacktrace=None,
                 exception: Exception = None, **metadata):
        self.stack_trace_list = []
        if exception is not None:
            if isinstance(exception, TestException):
                self.stack_trace_list.extend(exception.stack_trace_list)
            else:
                self.stack_trace_list.append(
                    {'err_code': '', 'msg': str(exception), 'err_class': exception.__class__.__name__,
                     'stacktrace': str(exception),
                     'metadata': {}})
        # 自定义的报错信息在最上层
        # modified by james at 20200813
        if not isinstance(err_class, str):
            err_class = str(err_class)
        self.stack_trace_list.insert(0, {'err_code': err_code, 'msg': msg, 'err_class': err_class,
                                         'stacktrace': stacktrace,
                                         'metadata': metadata})

    @classmethod
    def by_error_code(cls, code: ErrorCode, exception: Exception = None, **message_params):
        """init the exception from the error code
        :param code: 错误码
        :param exception: 底层错误
        :param message_params: 消息参数，key=value形式，要与消息模板中的key对应上
        :return: TestException
        """
        error_code = code.value
        error_type = ErrorType.get_type_from_code(code)
        error_message = Msg.get_error_message(code)

        # 如果需要组装消息，会尝试组装，如果key name对不上，会记录错误
        if message_params:
            try:
                error_message = error_message.format(**message_params)
            except KeyError as e:
                logger.error('the key {0} not in language string {1}'.format(e, error_message))

        return TestException(error_message, error_code, error_type, None, exception)

    @classmethod
    def by_exception(cls, exception: Exception):
        if exception is None:
            raise cls.by_error_code(ErrorCode.EXCEPTION_EMPTY)
        if isinstance(exception, TestException):
            return exception

        # Add by james at 20200825
        # 对Selenium内部错误进一步封装
        if isinstance(exception, NoSuchElementException):
            # 如果组件没找到，认为是页面功能异常
            msg = str(exception)
            # Message: no such element: Unable to locate element: {\"method\":\"xpath\",\"selector\":\"//li[contains
            # (@class,'js-second-level-menu-list')]/a/span/span[@title='CPC To Do(New)']/parent::*/parent::*\"}\n
            # (Session info: chrome=84.0.4147.135)\n"
            selector_type = cls._get_sub_string(msg, '"method":"', '",')
            selector = cls._get_sub_string(msg, '"selector":"', '"}\n')
            return cls.by_error_code(ErrorCode.NOT_LOCATE_ELEMENT, exception, selector_type=selector_type, selector=selector)
        elif isinstance(exception, TimeoutException):
            return cls.by_error_code(ErrorCode.ELEMENT_TIME_OUT, exception)
        elif isinstance(exception, InvalidElementStateException):
            return cls.by_error_code(ErrorCode.ELEMENT_STATE_INVALID, exception)
        elif isinstance(exception, StaleElementReferenceException):
            return cls.by_error_code(ErrorCode.ELEMENT_STALE_REFERENCE, exception)
        elif isinstance(exception, NoSuchAttributeException):
            return cls.by_error_code(ErrorCode.ELEMENT_NO_ATTRIBUTE, exception)
        elif isinstance(exception, ElementNotVisibleException):
            return cls.by_error_code(ErrorCode.ELEMENT_NO_VISIBLE, exception)
        elif isinstance(exception, ElementNotInteractableException):
            return cls.by_error_code(ErrorCode.ELEMENT_NO_INTERACTABLE, exception)
        elif isinstance(exception, ElementClickInterceptedException):
            return cls.by_error_code(ErrorCode.ELEMENT_CLICK_INTERCEPTED, exception)
        else:
            return cls.by_error_code(ErrorCode.UNKNOWN, exception, value=str(exception))

    def __str__(self):
        # 添加对中文的支持 modified by james at 20200807
        return json.dumps(self.stack_trace_list, ensure_ascii=False)

    @staticmethod
    def _get_sub_string(msg, start, end):
        if not msg:
            return None

        if start is None:
            return None

        pos_start = msg.find(start)
        if pos_start < 0:
            return None

        pos_start += len(start)

        if end is None:
            pos_end = len(msg)
        else:
            pos_end = msg.find(end, pos_start)
            if pos_end < 0:
                pos_end = len(msg)

        return msg[pos_start:pos_end]


if __name__ == "__main__":
    # print(TestException.by_error_code(ErrorCode.BUSINESS_FLOW_ERROR))
    # print(TestException.by_exception(RuntimeError('for test')))
    # print(TestException('init from error', err_class='test'))
    # xx = TestException(msg="6", error_code="4", err_class="5")
    # print(xx)
    # print(xx.stack_trace_list[0]['msg'])
    # print(TimeoutException().__class__.__name__)
    # yy = TestException(msg="1", error_code="2", err_class="3", exception=TimeoutException())
    # print(yy)
    # try:
    #     2 / 0
    # except Exception as e:
    #     print(create_exception(e))
    message = "Message: no such element: Unable to locate element: {\"method\":\"xpath\",\"selector\":\"//li[contains(@class,'js-second-level-menu-list')]/a/span/span[@title='CPC To Do(New)']/parent::*/parent::*\"}\n  (Session info: chrome=84.0.4147.135)\n"
    print(TestException._get_sub_string(message, 'method":"', '",}'))
    selector = TestException._get_sub_string(message, '"selector":"', '"}\n')
    print(selector)
